package com.bankcard.bininfo;

import com.bankcard.bininfo.model.*;
import com.bankcard.bininfo.storage.*;
import com.bankcard.bininfo.trie.MemoryTrieStorage;
import com.bankcard.bininfo.trie.RedisTrieStorage;
import com.bankcard.bininfo.trie.TrieStorage;
import com.bankcard.util.RedisClient;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * BinInfoRepository is a singleton class responsible for managing and retrieving BIN (Bank Identification Number) information.
 * It provides efficient storage and lookup of BIN data using a combination of concurrent data structures and a trie data structure.
 *
 * <p>This repository is thread-safe and optimized for high-concurrency scenarios, utilizing {@link CopyOnWriteArrayList}
 * for fast lookups and a custom trie implementation for efficient prefix matching.</p>
 *
 * <p><strong>Usage:</strong></p>
 * <ul>
 *   <li>Use {@link #getInstance()} to get the singleton instance of this repository.</li>
 *   <li>Use {@link #addBinInfo(BinInfo)} to add new BIN information to the repository.</li>
 *   <li>Use {@link #getBinInfo(String)} to retrieve BIN information for a given account number.</li>
 * </ul>
 *
 * <p><strong>Note:</strong> This class is designed to be used as a singleton. Do not attempt to create instances
 * directly using the constructor.</p>
 *
 * <p><strong>Example usage:</strong></p>
 * <pre>
 * {@code
 * // Get the singleton instance
 * BinInfoRepository repository = BinInfoRepository.getInstance();
 *
 * // Add BIN information
 * BinInfo binInfo = new BinInfo.Builder("123456")
 *     .cardBrand(CardBrand.VISA)
 *     .cardType(CardType.CREDIT)
 *     .cardLevel(CardLevel.PLATINUM)
 *     .issuerInfo(new IssuerInfo("Bank Name", "Bank URL", "Bank Phone", "Bank Short Name", new CountryInfo("US", "USA", "United States", "840")))
 *     .build();
 * repository.addBinInfo(binInfo);
 *
 * // Retrieve BIN information
 * String accountNumber = "1234567890123456";
 * BinInfo retrievedBinInfo = repository.getBinInfo(accountNumber);
 * if (retrievedBinInfo != null) {
 *     System.out.println("Card Brand: " + retrievedBinInfo.getCardBrand());
 *     System.out.println("Issuer: " + retrievedBinInfo.getIssuerInfo().getIssuerName());
 * } else {
 *     System.out.println("No BIN information found for this account number.");
 * }
 * }
 * </pre>
 *
 * @see BinInfo
 * @see IssuerInfo
 * @see CardBrand
 * @see CardType
 * @see CardLevel
 */
public class BinInfoRepository {
    private static BinInfoRepository INSTANCE;
    private final BinInfoStorage storage;
    private final TrieStorage trieStorage;
    private static final CopyOnWriteArrayList<BinRange> quickMatchRanges = new CopyOnWriteArrayList<>();

    static {
        loadInitialQuickMatchData();
    }

    private BinInfoRepository(StorageProvider provider) {
        this.storage = provider.createBinInfoStorage();
        this.trieStorage = provider.createTrieStorage();
    }

    public static synchronized BinInfoRepository getInstance() {
        if (INSTANCE == null) {
            StorageProvider provider = loadStorageProvider();
            INSTANCE = new BinInfoRepository(provider);
        }
        return INSTANCE;
    }

    private static StorageProvider loadStorageProvider() {
        ServiceLoader<StorageProvider> loader = ServiceLoader.load(StorageProvider.class);
        Iterator<StorageProvider> iterator = loader.iterator();
        return iterator.hasNext() ? iterator.next() : new MemoryStorageProvider();
    }

    public void addBinInfo(BinInfo binInfo) {
        storage.addBinInfo(binInfo);
        trieStorage.insertIntoTrie(binInfo.getBin());
    }

    public BinInfo getBinInfo(String accountNumber) {
        for (BinRange range : quickMatchRanges) {
            if (range.matches(accountNumber)) {
                IssuerInfo issuerInfo = range.getIssuerCard().getIssuerInfo();
                return new BinInfo.Builder(accountNumber.substring(0, 6))
                        .cardBrand(range.getIssuerCard().getCardBrand())
                        .issuerInfo(issuerInfo)
                        .build();
            }
        }

        // If quick match fails, use existing trie-based lookup
        String matchedBin = trieStorage.findLongestMatch(accountNumber);
        if (matchedBin != null) {
            return storage.getBinInfo(matchedBin);
        }
        return null;
    }

    public void updateBinInfo(BinInfo binInfo) {
        storage.updateBinInfo(binInfo);
    }

    public List<BinInfo> getAllBinInfo() {
        return storage.getAllBinInfo();
    }

    // Add method to dynamically update quick match data
    public static void updateQuickMatchData(String jsonString) {
        loadQuickMatchDataFromJson(jsonString);
    }

    private static void loadInitialQuickMatchData() {
        // Load hardcoded data using IssuerCards
        addQuickMatchRange(IssuerCards.BOA_VISA, "622126-622925");
        addQuickMatchRange(IssuerCards.CHASE_MASTER, "60400100-60420099");
    }

    public static void addQuickMatchRanges(String issuerCard, List<String> ranges) {
        for (String range : ranges) {
            addQuickMatchRange(IssuerCards.valueOf(issuerCard), range);
        }
    }

    public static void addQuickMatchRanges(String issuerCard, String ... ranges) {
        for (String range : ranges) {
            addQuickMatchRange(IssuerCards.valueOf(issuerCard), range);
        }
    }

    public static void addQuickMatchRanges(IssuerCards.IssuerCard issuerCard, String ... ranges) {
        for (String range : ranges) {
            addQuickMatchRange(issuerCard, range);
        }
    }

    private static void addQuickMatchRanges(IssuerCards.IssuerCard issuerCard, List<String> ranges) {
        for (String range : ranges) {
            addQuickMatchRange(issuerCard, range);
        }
    }

    private static void addQuickMatchRange(IssuerCards.IssuerCard issuerCard, String range) {
        String[] parts = range.split("-");
        String start = parts[0];
        String end = parts.length > 1 ? parts[1] : start;
        quickMatchRanges.add(new BinRange(start, end, issuerCard));
    }

    /**
     * Loads quick match data from a JSON string.
     *
     * @param jsonString A JSON string containing BIN ranges and their corresponding issuer cards.
     *                   The format should be a map where keys are BIN ranges (single or hyphenated)
     *                   and values are issuer card identifiers.
     *
     * <p>Example usage:</p>
     * <pre>
     * {@code
     * String jsonData = "{\"622126-622925\":\"BOA_VISA\",\"60400100-60420099\":\"CHASE_MASTER\","
     *                 + "\"506099-506198,650002-650027,507865-507964\":\"ABC_VERVE\","
     *                 + "\"50,56,58,60-63\":\"ABC_GPN\"}";
     *
     * loadQuickMatchDataFromJson(jsonData);
     * }
     * </pre>
     *
     * <p>This example loads the following mappings:
     * <ul>
     * <li>BIN range 622126-622925 to BOA_VISA</li>
     * <li>BIN range 60400100-60420099 to CHASE_MASTER</li>
     * <li>Multiple BIN ranges (506099-506198, 650002-650027, 507865-507964) to ABC_VERVE</li>
     * <li>Multiple single BINs and a range (50, 56, 58, 60-63) to ABC_GPN</li>
     * </ul>
     */
    private static void loadQuickMatchDataFromJson(String jsonString) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            Map<String, String> jsonData = mapper.readValue(jsonString, new TypeReference<Map<String, String>>() {});
            for (Map.Entry<String, String> entry : jsonData.entrySet()) {
                String ranges = entry.getKey();
                String cardBrand = entry.getValue();
                for (String range : ranges.split(",")) {
                    addQuickMatchRange(new IssuerCards.IssuerCard(CardBrands.valueOf(cardBrand.toUpperCase()), Issuers.valueOf(range.split("-")[0])), range);
                }
            }
        } catch (Exception e) {
            // Log error or handle exception
        }
    }

    private static class BinRange {
        private final String start;
        private final String end;
        private final IssuerCards.IssuerCard issuerCard;

        BinRange(String start, String end, IssuerCards.IssuerCard issuerCard) {
            this.start = start;
            this.end = end;
            this.issuerCard = issuerCard;
        }

        boolean matches(String accountNumber) {
            int compareLength = Math.min(Math.min(start.length(), end.length()), accountNumber.length());
            String truncatedAccountNumber = accountNumber.substring(0, compareLength);
            return truncatedAccountNumber.compareTo(start.substring(0, compareLength)) >= 0 &&
                    truncatedAccountNumber.compareTo(end.substring(0, compareLength)) <= 0;
        }

        IssuerCards.IssuerCard getIssuerCard() {
            return issuerCard;
        }
    }
}